/*           Copyright(c) 2005-2008 Dave Sexton            *
 *                   HtmlEditor Control                    *
 * http://www.codeplex.com/DocProject/Project/License.aspx *
 ***********************************************************/
using System;
using System.Collections.Generic;
using System.Text;
using System.IO;
using ComTypes = System.Runtime.InteropServices.ComTypes;
using mshtml;
using DaveSexton.Controls.Native;
using System.Runtime.InteropServices;
using DaveSexton.Controls.Resources;

namespace DaveSexton.Controls
{
	public enum HtmlStreamAccess
	{
		/// <summary>
		/// When passed to the constructor of the <see cref="HtmlStream" /> class, the new object will load the HTML from the persistance 
		/// stream, specified when the object is constructed.  The HTML is readable, but not writable.
		/// </summary>
		Read,
		/// <summary>
		/// When passed to the constructor of the <see cref="HtmlStream" /> class, any HTML written to the new object will be written to the 
		/// underlying persistance stream, specified when the object is constructed, when the <see cref="HtmlStream.Flush" /> method is called 
		/// and when the object is disposed.  Data that is written to the stream is also readable; however, the HTML data of the underlying 
		/// persistence stream is not.
		/// </summary>
		Write
	}

	public sealed class HtmlStream : MemoryStream, ComTypes.IStream
	{
		#region Public Properties
		public bool IsDirty
		{
			get
			{
				return persistStreamInit.IsDirty() == Constants.S_OK;
			}
		}

		public HtmlStreamAccess Access
		{
			get
			{
				return access;
			}
		}

		public override bool CanWrite
		{
			get
			{
				return access == HtmlStreamAccess.Write && base.CanWrite;
			}
		}

		public override long Position
		{
			get
			{
				return base.Position;
			}
			set
			{
				if (access != HtmlStreamAccess.Write)
					throw new InvalidOperationException(Errors.HtmlStreamNotWritable);

				base.Position = value;
			}
		}
		#endregion

		#region Private / Protected
		private readonly IPersistStreamInit persistStreamInit;
		private readonly Encoding encoding;
		private readonly HtmlStreamAccess access;
		private readonly bool setDirtyFlagOnly;
		#endregion

		#region Constructors
		/// <summary>
		/// Constructs a new instance of the <see cref="HtmlStream" /> class.
		/// </summary>
		public HtmlStream(Encoding encoding, object persistStreamInit, bool clearDirty, HtmlStreamAccess access)
		{
			if (encoding == null)
				throw new ArgumentNullException("encoding");

			if (persistStreamInit == null)
				throw new ArgumentNullException("persistStreamInit");

			this.persistStreamInit = persistStreamInit as IPersistStreamInit;

			if (this.persistStreamInit == null)
				throw new ArgumentException(Errors.InvalidComStreamArgument);

			if (!Enum.IsDefined(typeof(HtmlStreamAccess), access))
				throw new System.ComponentModel.InvalidEnumArgumentException("access", (int) access, typeof(HtmlStreamAccess));

			this.encoding = encoding;
			this.access = access;

			if (access == HtmlStreamAccess.Read)
				LoadHtml(clearDirty);
		}

		/// <summary>
		/// Constructs a new instance of the <see cref="HtmlStream" /> class.
		/// </summary>
		public HtmlStream(object persistStreamInit, bool clearDirty, HtmlStreamAccess access)
		{
			if (persistStreamInit == null)
				throw new ArgumentNullException("persistStreamInit");

			this.persistStreamInit = persistStreamInit as IPersistStreamInit;

			if (this.persistStreamInit == null)
				throw new ArgumentException(Errors.InvalidComStreamArgument);

			if (!Enum.IsDefined(typeof(HtmlStreamAccess), access))
				throw new System.ComponentModel.InvalidEnumArgumentException("access", (int) access, typeof(HtmlStreamAccess));

			this.access = access;

			if (access == HtmlStreamAccess.Read)
				LoadHtml(clearDirty);
		}

		/// <summary>
		/// Constructs a new instance of the <see cref="HtmlStream" /> class.
		/// </summary>
		private HtmlStream(IPersistStreamInit persistStreamInit)
		{
			this.persistStreamInit = persistStreamInit;
			setDirtyFlagOnly = true;
		}

		/// <summary>
		/// Constructs a new instance of the <see cref="HtmlStream" /> class.
		/// </summary>
		private HtmlStream(byte[] buffer, bool writable)
			: base(buffer, writable)
		{
		}
		#endregion

		#region Methods
		public static void ClearIsDirty(object persistStreamInit)
		{
			if (persistStreamInit == null)
				throw new ArgumentNullException("persistStreamInit");

			IPersistStreamInit stream = persistStreamInit as IPersistStreamInit;

			if (stream == null)
				throw new ArgumentException(Errors.InvalidComStreamArgument);

			ClearIsDirty(stream);
		}

		internal static void ClearIsDirty(IPersistStreamInit persistStreamInit)
		{
			using (HtmlStream stream = new HtmlStream(persistStreamInit))
			{
				stream.ClearIsDirty();
			}
		}

		private void ClearIsDirty()
		{
			// the IStream.Write implementation will just swallow the byte[] array and return immediately
			// when it's called by the Save method
			this.persistStreamInit.Save(this, Constants.TRUE);
		}

		private void LoadHtml(bool clearDirty)
		{
			// save the Html to this stream
			this.persistStreamInit.Save(this, (clearDirty) ? Constants.TRUE : Constants.FALSE);

			// must use base because the HtmlStream is not writeable and overrides Position
			base.Position = 0;
		}

		public override void Flush()
		{
			if (access != HtmlStreamAccess.Write)
				throw new InvalidOperationException(Errors.HtmlStreamNotWritable);

			if (persistStreamInit.InitNew() != Constants.S_OK)
				throw new InvalidOperationException(Errors.CannotInitializeDocument);

			long loadPosition = base.Position;
			base.Position = 0;

			// Calling Load will cause IStream.Read (implemented below) to be invoked repeatedly until the entire stream has been read.
			// Position is set to zero first so that the entire stream is read.
			persistStreamInit.Load(this);

			base.Position = loadPosition;
		}

		public void Write(string html)
		{
			if (access != HtmlStreamAccess.Write)
				throw new InvalidOperationException(Errors.HtmlStreamNotWritable);

			Encoding enc = encoding ?? Encoding.Unicode;

			byte[] bytes = enc.GetBytes(html);

			base.Write(bytes, 0, bytes.Length);
		}

		public override void Write(byte[] buffer, int offset, int count)
		{
			if (access != HtmlStreamAccess.Write)
				throw new InvalidOperationException(Errors.HtmlStreamNotWritable);

			base.Write(buffer, offset, count);
		}

		public override void WriteByte(byte value)
		{
			if (access != HtmlStreamAccess.Write)
				throw new InvalidOperationException(Errors.HtmlStreamNotWritable);

			base.WriteByte(value);
		}

		public override IAsyncResult BeginWrite(byte[] buffer, int offset, int count, AsyncCallback callback, object state)
		{
			if (access != HtmlStreamAccess.Write)
				throw new InvalidOperationException(Errors.HtmlStreamNotWritable);

			return base.BeginWrite(buffer, offset, count, callback, state);
		}

		public string ReadToEnd()
		{
			Encoding enc = encoding ?? Encoding.Unicode;

			long count = Length - Position;

			if (count <= 0)
				return null;

			byte[] bytes = new byte[count];

			int read = 0;
			do
			{
				read = Read(bytes, read, (int) (count - read));
			}
			while (Position < Length);

			return enc.GetString(bytes);
		}

		protected override void Dispose(bool disposing)
		{
			try
			{
				if (disposing && access == HtmlStreamAccess.Write)
					Flush();
			}
			finally
			{
				base.Dispose(disposing);
			}
		}
		#endregion

		#region IStream Members
		void ComTypes.IStream.Read(byte[] pv, int cb, IntPtr pcbRead)
		{
			int count = Read(pv, 0, cb);

			if (pcbRead != IntPtr.Zero)
				Marshal.WriteInt32(pcbRead, count);
		}

		void ComTypes.IStream.Write(byte[] pv, int cb, IntPtr pcbWritten)
		{
			if (!setDirtyFlagOnly)
				base.Write(pv, 0, cb);

			if (pcbWritten != IntPtr.Zero)
				Marshal.WriteInt32(pcbWritten, (setDirtyFlagOnly) ? cb : (int) Math.Min(cb, Length - Position));
		}

		void ComTypes.IStream.Seek(long dlibMove, int dwOrigin, IntPtr plibNewPosition)
		{
			SeekOrigin origin;

			switch ((STREAM_SEEK) dwOrigin)
			{
				case STREAM_SEEK.STREAM_SEEK_CUR:
					origin = SeekOrigin.Current;
					break;
				case STREAM_SEEK.STREAM_SEEK_END:
					origin = SeekOrigin.End;
					break;
				case STREAM_SEEK.STREAM_SEEK_SET:
					origin = SeekOrigin.Begin;
					break;
				default:
					throw new System.ComponentModel.InvalidEnumArgumentException("dwOrigin", dwOrigin, typeof(STREAM_SEEK));
			}

			Seek(dlibMove, origin);

			if (plibNewPosition != IntPtr.Zero)
			{
				ULARGE_INTEGER largeInt = new ULARGE_INTEGER();
				largeInt.QuadPart = unchecked((ulong) Position);

				Marshal.StructureToPtr(largeInt, plibNewPosition, true);
			}
		}

		void ComTypes.IStream.SetSize(long libNewSize)
		{
			SetLength(libNewSize);
		}

		void ComTypes.IStream.CopyTo(ComTypes.IStream pstm, long cb, IntPtr pcbRead, IntPtr pcbWritten)
		{
			if (pstm == null)
				throw new ArgumentNullException("pstm");

			int count = (int) Math.Min(cb, Length - Position);

			if (count < 0)
				throw new ArgumentOutOfRangeException("cb");

			byte[] buffer = GetBuffer();
			byte[] target = new byte[count];

			Buffer.BlockCopy(buffer, (int) Position, target, 0, count);

			pstm.Write(target, count, pcbWritten);

			if (pcbRead != IntPtr.Zero)
				Marshal.WriteInt32(pcbRead, count);
		}

		void ComTypes.IStream.Commit(int grfCommitFlags)
		{
		}

		void ComTypes.IStream.Revert()
		{
		}

		void ComTypes.IStream.LockRegion(long libOffset, long cb, int dwLockType)
		{
			Marshal.ThrowExceptionForHR(unchecked((int) 0x80030001));
		}

		void ComTypes.IStream.UnlockRegion(long libOffset, long cb, int dwLockType)
		{
			Marshal.ThrowExceptionForHR(unchecked((int) 0x80030001));
		}

		void ComTypes.IStream.Stat(out ComTypes.STATSTG pstatstg, int grfStatFlag)
		{
			pstatstg = new ComTypes.STATSTG();
			pstatstg.type = (int) STGTY.STGTY_STREAM;
			pstatstg.cbSize = Length;
			pstatstg.grfMode = (int) (STGM.STGM_READWRITE | STGM.STGM_SHARE_DENY_NONE | STGM.STGM_CREATE | STGM.STGM_DIRECT);

			if ((STATFLAG) grfStatFlag == STATFLAG.STATFLAG_DEFAULT)
				pstatstg.pwcsName = GetType().ToString();
		}

		void ComTypes.IStream.Clone(out ComTypes.IStream ppstm)
		{
			// NOTE: the cloned IStream is only writable if the managed stream is read-only
			ppstm = new HtmlStream(GetBuffer(), access == HtmlStreamAccess.Read);
		}
		#endregion
	}
}
